# GAMEOBJ.PY - Game agents
# HIT3046 AI for GAMES
# ------- Assignment 2
# Authors: Mike Blackney, 0906123
#          Alex Bruce, 5409438

import math
import pygame
from pygame.locals import *  # Keypress constants

import pos        # Vectors

import io         # Sprite classes
import gob        # Goal oriented abstract classes
import gameaction # Game agent actions
import gamemap    # Map and IM class
import utility

from defaults import *

# class SPY
# This class represents the player agent.  Controlled by the mouse
# and keypresses.
class SPY (gob.GAMEAGENT):
    "The player is a spy.  The spy is controlled by the mouse and WASD keyboard."
    
    def __init__( self, *args ):
        if len( args ) is not 1: #expect: location as tuple (x,y)
            print "Incorrect arguments sent to GUARD init."
            raise SystemExit
        # Chain up:
        gob.GAMEAGENT.__init__( self, args )
        # Set the location:
        self.location = pos.Pos(args[0])
        # Facing North:
        self.r_facing  = 0
        # Initialise the facing vectors:
        self.MouseMove( (0,0) )
        self.velocity = pos.Pos( )
        # Set the sprites:
        self.spr_sprite = self.SetSprite( \
            io.GAMESPRITE( self, "spy.gif", "spy_.gif", 0 ) )
        self.spr_sprite.Position( args[0] )
        self.spr_reticle = self.SetSprite( \
            io.SPRITE( "spy_reticle.gif", 0 ) )
        # And without a key depressed:
        self.k_forward = False
        self.k_back    = False
        self.k_left    = False
        self.k_right   = False
        self.m_left    = False
        self.m_right   = False
        self.m_middle  = False

        # With full health:
        self.wounds = 0
        # Players have speed limits on shooting and placing bombs:
        self.shoot_timeout = 0
        self.bomb_timeout  = 0
        
        # We begin in 'normal' action:
        self.PushAction( gameaction.IDLE( ) )

    def Destroy( self ): # When called, removes from the simulation
        # Clear the reticle sprite:
        self.spr_reticle = self.ClearSprite( self.spr_reticle )
        # Chain up:
        gob.GAMEAGENT.Destroy( self )

    def MouseMove( self, delta ):
        # Set our facing:
        self.r_facing += delta[0] * MOUSE_SENSITIVITY
        # Normalise to 0..theta..2*pi:
        while self.r_facing > 2 * math.pi: self.r_facing -= 2 * math.pi
        while self.r_facing < 0: self.r_facing += 2 * math.pi
        # Set our forward and right unit vectors:
        self.v_forward = pos.Pos( math.cos(self.r_facing), \
                                  math.sin(self.r_facing) )
        self.v_right = pos.Pos( -self.v_forward.y, self.v_forward.x )

    def MousePress( self ):
        m_left, m_middle, m_right = pygame.mouse.get_pressed()
        if not self.m_left and m_left:
            self.m_left = m_left
            # Leftclick.  Fire a bullet:
            if self.shoot_timeout <= 0:
                self.ShootRel( self.v_forward * BULLET_RANGE )
                self.shoot_timeout = SHOOT_TIMEOUT
        if not self.m_right and m_right:
            self.m_right = m_right
            # Rightclick.  Drop a bomb:
            if self.bomb_timeout <= 0:
                EXPLOSIVES( self.location )
                self.bomb_timeout = BOMB_TIMEOUT
        if not self.m_middle and m_middle:
            # Middleclick.
            self.m_middle = m_middle

    def MouseRelease( self ):
        m_left, m_middle, m_right = pygame.mouse.get_pressed()
        if self.m_left and not m_left:
            self.m_left = m_left
        if self.m_right and not m_right:
            self.m_right = m_right
        if self.m_middle and not m_middle:
            self.m_middle = m_middle

    def KeyPress( self, key ):
        if key == K_w:
            self.k_forward = True
        elif key == K_a:
            self.k_left    = True
        elif key == K_s:
            self.k_back    = True
        elif key == K_d:
            self.k_right   = True

    def KeyRelease( self, key ):
        if key == K_w:
            self.k_forward = False
        elif key == K_a:
            self.k_left    = False
        elif key == K_s:
            self.k_back    = False
        elif key == K_d:
            self.k_right   = False

    def ShootRel( self, vector ):
        # Fire a bullet at an angle relative to the actor:
        BULLET( self.location, \
                self.location + vector,
                self )
        
    def TakeDamage( self, amount ):
        # The spy has been shot!
        self.wounds += amount

    def Tick( self, CurrTime, deltaTime ):
        # Update the velocity based on last control key pressed:
        self.velocity.zero( )
        if self.k_forward == True:
            self.velocity +=  self.v_forward
        if self.k_left == True:
            self.velocity += -self.v_right
        if self.k_back == True:
            self.velocity += -self.v_forward
        if self.k_right == True:
            self.velocity +=  self.v_right

        # Find out if we can move to the new location:
        new_location = self.location \
                       + self.velocity.normalise( ) * SPY_SPEED * deltaTime;
        if self.ValidMove( new_location ):
            # Move at current velocity:
            self.location = new_location
        # If we can't move directly, try each of x and y components alone:
        else:
            x_slide = pos.Pos( self.location )
            x_slide.x = new_location.x
            y_slide = pos.Pos( self.location )
            y_slide.y = new_location.y
            
            if self.ValidMove( x_slide ):
                self.location.x = new_location.x
            elif self.ValidMove( y_slide ):
                self.location.y = new_location.y

        # Timeout after shooting and placing bombs:
        self.shoot_timeout = max( self.shoot_timeout - deltaTime, 0 )
        self.bomb_timeout  = max( self.bomb_timeout  - deltaTime, 0 )

        # Display the reticle:
        reticle_loc = self.location + self.v_forward * RETICLE_DIST;
        self.spr_reticle.rect.center = reticle_loc.to_list( )
            
        # Chain up for action operation:
        gob.GAMEAGENT.Tick( self, CurrTime, deltaTime )

    def ValidMove( self, loc ):
        return not gamemap.MAP( ).isBlocked( utility.ScreenToMap( loc ) ) \
               and loc.x >= 0 and loc.x < SCREEN_SIZE[0] \
               and loc.y >= 0 and loc.y < SCREEN_SIZE[1]

# class Guard
# Guards patrol the base looking for the player.
class GUARD (gob.GAMEAGENT):
    "An evil, faceless guard trying to stop our hero."

    def __init__( self, *args ):
        if len( args ) is not 1: #expect: location as tuple (x,y)
            print "Incorrect arguments sent to GUARD init."
            raise SystemExit
        # Chain up:
        gob.GAMEAGENT.__init__(self, args)
        # Set the location:
        self.location = pos.Pos(args[0])
        # Three desires:
        self.d_strength   = 0
        self.d_security   = 0
        self.d_discontent = 0
        # Wounds:
        self.wounds = 0
        # Can't see any players or bombs:
        self.vis_spy = None
        self.vis_bombs = []
        # Set the sprites:
        self.spr_sprite = self.SetSprite( \
            io.GAMESPRITE( self, "guard_icon.gif", "guard_icon.gif", 0 ) )
        # Initial actions:
        self.PushAction( gameaction.DISARM( ) )
        self.PushAction( gameaction.ENGAGE( ) )
        self.PushAction( gameaction.HEAL( ) )
        self.PushAction( gameaction.PATROL( ) )

    def PrintText( self, font, background ):
        strings = ["Str " + str(self.d_strength), "Sec "+str(self.d_security)]
        for i in range(2):
            text = font.render( strings[i], 1, (255,255,255) )
            textpos = text.get_rect(centerx=self.spr_sprite.rect.center[0], \
                                    centery=self.spr_sprite.rect.center[1]+i*10+20 )
            background.blit( text, textpos )

    def TakeDamage( self, amount ):
        # Guard gets wounded:
        self.wounds += amount
        if self.wounds > GUARD_FATAL:
            self.Destroy( )

    def ShootRel( self, vector ):
        # Fire a bullet at an angle relative to the actor:
        BULLET( self.location, \
                self.location + vector,
                self )

    def Tick( self, CurrTime, deltaTime ):
        # Make a list of all the player(s) and bombs that the guard can see:
        allactors = gob.GAMEAGENT.agent_list
        self.vis_spy = None
        while len(self.vis_bombs) > 0: self.vis_bombs.pop( )
        for actor in allactors:
            if actor.__class__.__name__ == "SPY" \
               and self.TraceLOS(actor):
                self.vis_spy = actor
            elif actor.__class__.__name__ == "EXPLOSIVES"\
               and self.TraceLOS(actor):
                self.vis_bombs.append(actor)
        # Increase the security need based on visible enemies:
        insecurity = len(self.vis_bombs) - 0.5
        if self.vis_spy is not None: insecurity += 1
        insecurity *= deltaTime
        self.d_security += insecurity
        # Set the strength need based on wounds:
        self.d_strength = self.wounds * WOUNDS_MULT + DESIRE_MIN
        # Ensure we don't have negative desires:
        self.d_strength = max(DESIRE_MIN, self.d_strength)
        self.d_security = max(DESIRE_MIN, self.d_security)
        # Record current time in our tile:
        for y in range(-3, 4):
            for x in range(-3, 4):
                gamemap.MAP( ).SetTime( \
                    utility.ScreenToMap( self.location ) + pos.Pos(x,y), \
                    CurrTime )
        # Chain up for action operation:
        gob.GAMEAGENT.Tick( self, CurrTime, deltaTime )

    def TraceLOS( self, actor ):
        # Trace line-of-sight to an actor.  Uses a simple line-drawing
        # algorithm, checking with low resolution against the map array.
        # First check to see if the distance is greater than our view range:
        if (actor.location - self.location).length( ) > GUARD_VIEW_DIST:
            return False
        # We're close, so trace the line:
        delta = utility.ScreenToMap( actor.location ) \
                - utility.ScreenToMap( self.location )
        maplocation = utility.ScreenToMap( self.location )
        steps = max( abs(delta.x), abs(delta.y) ) 
        if steps <= 1: return True # One or fewer steps means we're touching
        delta.force_float( )
        delta /= steps
        for tile_num in range( steps ):
            tile = maplocation + delta * tile_num
            gamemap.MAP( ).SetLos( tile.round( ) )
            if gamemap.MAP( ).isBlocked( tile.round( ) ): return False

        return True

    def MoveTo( self, target, deltaTime, flee=False ):
        # If it's too small a distance, don't move:
        if (target - self.location).length( ) < MOVE_CLOSE_ENOUGH:
            return
        # Move 'speed' units towards target location
        velocity = (target - self.location).normalise( )
        # Find out if we can move to the new location:
        new_location = self.location \
                       + velocity.normalise( ) * self.GetSpeed( ) * deltaTime;
        if self.ValidMove( new_location ):
            # Move at current velocity:
            self.location = new_location
        # If we can't move directly, try each of x and y components alone:
        else:
            x_slide = pos.Pos( self.location )
            x_slide.x = new_location.x
            y_slide = pos.Pos( self.location )
            y_slide.y = new_location.y
            
            if self.ValidMove( x_slide ):
                self.location.x = new_location.x
            elif self.ValidMove( y_slide ):
                self.location.y = new_location.y

    def ValidMove( self, loc ):
        return not gamemap.MAP( ).isBlocked( utility.ScreenToMap( loc ) ) \
               and loc.x >= 0 and loc.x < SCREEN_SIZE[0] \
               and loc.y >= 0 and loc.y < SCREEN_SIZE[1]


# Simple objects
# Actionless game agents include bullets, bombs and buildings.
class SIMPLEOBJECT (gob.GAMEAGENT):
    "Visual Effect and other simple objects derive from this class."
    def __init__(self, *args):
        # Chain up:
        gob.GAMEAGENT.__init__(self, args)
        assert len(args) == 2 # expect: filename for image, location
        self.spr_sprite = self.SetSprite( io.SPRITE( args[0], 0 ) )
        self.location = args[1]
        self.PositionSprites( )
        # SimpleObjects don't have actions:
        self.PushAction( gameaction.IDLE( ) )


class EXPLOSIVES (SIMPLEOBJECT):
    "Explosives are set by Saboteurs, defused by Guards and can destroy locations."
    def __init__(self, *args):
        assert len(args) == 1 # expect: my loc
        SIMPLEOBJECT.__init__( self, "item_bomb.gif", args[0] )
        self.sound_count = 0
        self.countdown = BOMB_COUNTDOWN
        self.defuser = None
    def Tick( self, currTime, deltaTime ):
        # Chain up:
        SIMPLEOBJECT.Tick(self, currTime, deltaTime )
        # Bombs tick loudly:
        self.sound_count += deltaTime
        if (self.sound_count >= 1):
            self.sound_count = 0
            io.FEEDBACK( ).snd_tick.play( )
        # and count down before exploding:
        self.countdown -= deltaTime
        if self.countdown <= 0:
            io.FEEDBACK( ).snd_explosion.play( )
            # Iterate the agents and damage as appropriate:
            for agent in gob.GAMEAGENT.agent_list:
                if (self.location - agent.location).length( ) < BLAST_RADIUS:
                    # Damage guards and spies:
                    if agent.__class__.__name__ == "GUARD":
                        agent.TakeDamage( BOMB_DAMAGE )
                        # and destroy self:
                        self.Destroy( )
                        return
                    elif agent.__class__.__name__ == "SPY":
                        agent.TakeDamage( BOMB_DAMAGE )
                        # and destroy self:
                        self.Destroy( )
                        return
                    elif agent.__class__.__name__ == "BUILDING":
                        # If it's a building, destroy it:
                        agent.Destroy( )
            self.Destroy( )


class BULLET (SIMPLEOBJECT):
    "Bullets are fired by spies and guards."
    def __init__(self, *args):
        assert len(args) == 3 # expect: my loc, target loc as vectors, owner
        SIMPLEOBJECT.__init__( self, "item_bullet.gif", args[0] )
        self.fired_from = args[0]
        self.target = args[1]
        self.owner = args[2]
        # Play a sound:
        io.FEEDBACK( ).snd_pistol.play( )
    def Tick( self, currTime, deltaTime ):
        # Chain up:
        SIMPLEOBJECT.Tick(self, currTime, deltaTime )
        # Move to the target location:
        self.MoveTo( self.target, deltaTime )
        # If we're at the target, destroy: 
        if abs( self.location.x - self.target.x ) < BULLET_CLOSE_ENOUGH and \
           abs( self.location.y - self.target.y ) < BULLET_CLOSE_ENOUGH:
            self.Destroy( )
            return
        # If we've hit a guard or spy or building:
        for agent in gob.GAMEAGENT.agent_list:
            if agent is not self.owner and agent is not self \
               and self.isPenetrating(agent):
                # Damage guards and spies:
                if agent.__class__.__name__ == "GUARD":
                    gamemap.MAP( ).AddFrag( utility.ScreenToMap(agent.location), \
                                            DEATH_PENALTY )
                    agent.TakeDamage( BULLET_DAMAGE )
                    # and destroy self:
                    self.Destroy( )
                    return
                elif agent.__class__.__name__ == "SPY":
                    gamemap.MAP( ).AddFrag( utility.ScreenToMap(self.fired_from), \
                                            FRAG_BONUS )
                    agent.TakeDamage( BULLET_DAMAGE )
                    # and destroy self:
                    self.Destroy( )
                    return
                # Destroy self if we've hit a building...
                elif agent.__class__.__name__ == "BUILDING":
                    # And we've gone into invalid space:
                    if gamemap.MAP( ).isBlocked( \
                        utility.ScreenToMap( self.location ) ):
                        self.Destroy( )
                    
    def GetSpeed( self ):
        return BULLET_SPEED


class BUILDING (SIMPLEOBJECT):
    "Buildings block movement and can heal/reassure guards."
    def __init__(self, *args):
        assert len(args) == 2 # expect: building info dict and location
        # Chain up:
        SIMPLEOBJECT.__init__(self,args[0]["sprite"],pos.Pos(args[1]))
        # First parameter is a dict and contains:
        self.name = args[0]["name"]
        self.heal_reward = args[0]["heal_reward"]
        self.security_reward = args[0]["security_reward"]
        # Add this location to the map's building list:
        gamemap.MAP( ).AddBuilding( self )
        # Set the map to be blocked in the building's areas:
        for y in range(4):
            for x in range(4):
                gamemap.MAP( ).BlockingTile( 
                    utility.ScreenToMap( self.location ) + pos.Pos(x-2,y-2) )
    
    # Buildings add their sprite to the background list:
    def SetSprite( self, sprite ):
        io.FEEDBACK( ).backgroundList.add( sprite )
        return sprite

    def ClearSprite( self, sprite ):
        io.FEEDBACK( ).backgroundList.remove( sprite )
        return None

    def Destroy( self ):
        # Set the map to be unblocked in the building's areas:
        for y in range(4):
            for x in range(4):
                gamemap.MAP( ).UnblockingTile( 
                    utility.ScreenToMap( self.location ) + pos.Pos(x-2,y-2) )
        SIMPLEOBJECT.Destroy( self )
        

    def ClosestPoint( self, agent ):
        # Return the closest point outside this building:
        location = pos.Pos( agent.location )
        location.x = max( location.x, self.location.x - 2.5 * MAP_SCALE )
        location.x = min( location.x, self.location.x + 2.5 * MAP_SCALE )
        location.y = max( location.y, self.location.y - 2.5 * MAP_SCALE )
        location.y = min( location.y, self.location.y + 2.5 * MAP_SCALE )
        return utility.ScreenToMap( location )

